package ar.uba.fi.tonyvaliente.signature.query;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.StringTokenizer;

import ar.uba.fi.tonyvaliente.documents.Query;
import ar.uba.fi.tonyvaliente.documents.exception.QueryException;

/**
 * Representa una consulta booleana a ser ejecutada sobre
 * un indice soportado por un signature file. 
 * @author dmorello
 */
public class SignatureQuery implements Query {
	
	private SignatureFilter filter;
	
	/**
	 * Inicializa una nueva instancia con un filtro cualquiera
	 * @param filter un SignatureFilter cualquiera
	 */
	private SignatureQuery(SignatureFilter filter) {
		this.filter = filter;
	}
	
	/**
	 * Devuelve un objeto SignatureQuery correspondiente a la
	 * consulta en el string recibido por parametro.
	 * @param s String que contiene la consulta
	 * @return SignatureQuery correspondiente a la consulta en s
	 * @throws QueryException si hay un error en s
	 */
	public static SignatureQuery parse(String s) throws QueryException {
		
		// Divido la consulta en palabras
		String[] palabras = splitWords(s);
		if (palabras == null || palabras.length == 0) {
			throw new QueryException("La consulta booleana esta vacia");
		}
		
		// Obtengo el filtro para setear a la nueva instancia
		SignatureFilter filter = parse(palabras, 0, palabras.length);
		
		// Devuelvo una nueva instancia con el filtro obtenido
		return new SignatureQuery(filter);
	}
	
	@Override
	public Iterator<Integer> getResultIterator() {
		// TODO implementar esto!!
		return null;
	}
	
	@Override
	public String toString() {
		return "SignatureQuery: " + filter.toString();
	}
	
	/**
	 * Obtiene un filtro a partir de las palabras separadas
	 * de una consulta booleana. Toma un rango de posiciones
	 * del arreglo de palabras dado por los restantes parametros.
	 * 
	 * @param palabras arreglo de palabras de la consulta
	 * @param begin posicion del arreglo desde donde empieza a leer
	 * @param end posicion siguiente a la ultima que se debe incluir
	 * @return filtro correspondiente a la consulta
	 * @throws QueryException si hay algun error
	 */
	private static SignatureFilter parse(String[] palabras,
			int begin, int end) throws QueryException {
		
		Set<SignatureFilter> orFilters = new HashSet<SignatureFilter>();
		Set<SignatureFilter> andFilters = new HashSet<SignatureFilter>();
		
		// Inicializo flags
		boolean operacionActiva = true;
		boolean negate = false;
		
		int i = begin;
		while (i < end) {
			// Verifico si es un termino o un operador
			if (isTerm(palabras[i])) {
				// Es termino - Aplico stemming
				String term = applyStemming(palabras[i]);
				if (term == null) {
					throw new QueryException("Termino incorrecto: " + palabras[i]);
				}
				
				// Guardo como operando de un OR
				SignatureTermFilter filter = new SignatureTermFilter(term, negate);
				orFilters.add(filter);
				
				// Limpio flags
				negate = false;
				operacionActiva = false;
			} else {
				// Actuo de acuerdo al operador que este leyendo
				if (palabras[i].equalsIgnoreCase("AND")) {
					// Verifico que la palabra anterior no haya sido un AND u OR
					if (operacionActiva) {
						throw new QueryException("Error de sintaxis en la consulta");
					}
					
					// Genero el filtro OR que vincule todos los filtros en orFilters
					// y lo agrego al conjunto de operandos del AND
					SignatureFilter or = composeORFilter(orFilters, negate);
					andFilters.add(or);
					
					// Limpio el conjunto orFilters asi esta listo para lo que sigue
					orFilters.clear();
					
					// Seteo el flag para indicar que estoy en una operacion
					operacionActiva = true;
				} else if (palabras[i].equalsIgnoreCase("OR")) {
					// Verifico que la palabra anterior no haya sido un AND u OR
					if (operacionActiva) {
						throw new QueryException("Error de sintaxis en la consulta");
					}
					// Seteo el flag para indicar que estoy en una operacion
					operacionActiva = true;
				} else if (palabras[i].equalsIgnoreCase("NOT")) {
					// Invierto el valor del flag de negacion
					// Esto permite que varios NOT seguidos no den error
					negate = !negate;
					operacionActiva = true;
				} else if (palabras[i].equalsIgnoreCase("(")) {
					// Debe haber una operacion activa para proceder
					if (!operacionActiva) {
						throw new QueryException("Error de sintaxis en la consulta");
					}
					
					// Parseo la subconsulta en cuestion y la agrego al set
					int endSub = findSubqueryEnd(palabras, i+1);
					SignatureFilter sub = parse(palabras, i+1, endSub);
					if (negate) {
						sub.invertNegatedFlag();
					}
					orFilters.add(sub);
					
					// Actualizo variables y flags
					i = endSub;
					operacionActiva = false;
					negate = false;
					
				} else if (palabras[i].equalsIgnoreCase(")")) {
					// Error!
					throw new QueryException("Error de sintaxis en la consulta");
				}
				
			}
			
			// Paso a la siguiente palabra
			i++;
		}
		
		// Verifico que no haya quedado un operador suelto al final
		if (operacionActiva) {
			throw new QueryException("Error de sintaxis en la consulta");
		}
		
		// Genero un ORFilter para lo que haya quedado sin agregar
		SignatureFilter f = composeORFilter(orFilters, false);
		andFilters.add(f);
		
		// Uno todos los terminos con AND (miniterminos) y ya tengo mi filtro listo
		// Este filtro nunca va negado
		SignatureFilter and = composeANDFilter(andFilters, false);
		return and;
	}
	
	/**
	 * Separa en palabras el contenido de la query
	 * @param query Una query booleana
	 * @return array de Strings con palabras separadas
	 */
	private static String[] splitWords(String query) {
		// TODO: implementar esto correctamente
		
		// Convierto todo a minusculas
		query = query.toLowerCase();
		
		// Agrego espacios antes y despues de cada parentesis
		StringBuffer sb = new StringBuffer();
		sb.append(query);
		int start = 0;
		int pos;
		while ((pos = sb.indexOf("(", start)) != -1) {
			sb.replace(pos, pos+1, " ( ");
			start = pos + 3;
		}
		start = 0;
		while ((pos = sb.indexOf(")", start)) != -1) {
			sb.replace(pos, pos+1, " ) ");
			start = pos + 3;
		}
		query = sb.toString();
		System.out.printf("query: '%s'", query);
		System.out.println();
		System.out.flush();
		
		// Separo las palabras y devuelvo
		StringTokenizer tok = new StringTokenizer(query, " ");
		ArrayList<String> words = new ArrayList<String>();
		while (tok.hasMoreTokens()) {
			words.add(tok.nextToken());
		}
		String[] array = new String[words.size()];
		for (int i = 0; i < array.length; i++) {
			array[i] = words.get(i).trim();
		}
		return array;
	}
	
	/**
	 * Devuelve un boolean que indica si el string recibido
	 * es un termino (true) o un operador o parentesis (false)
	 * @param word un string cualquiera
	 * @return true si es termino, false si es operador o parentesis
	 */
	private static boolean isTerm(String word) {
		return !word.equalsIgnoreCase("AND")
				&& !word.equalsIgnoreCase("OR")
				&& !word.equalsIgnoreCase("NOT")
				&& !word.equals("(")
				&& !word.equals(")");
	}
	
	/**
	 * Aplica el filtro de stemming a un termino
	 * @param term un termino cualquiera
	 * @return termino resultante del stemming,
	 * o null si el stemming descarto dicho termino
	 */
	private static String applyStemming(String term) {
		// TODO: IMPLEMENTAR ESTOOOO
		return term;
	}
	
	/**
	 * Crea un SignatureFilter que vincule mediante el operador OR
	 * a todos los que contiene el Set recibido por parametro.
	 * Ademas este filtro estara negado o no segun el flag que tambien
	 * recibe como parametro.
	 * @param filters un conjunto de SignatureFilters
	 * @param negated flag de negacion para el filtro a generar
	 * @return SignatureFilter que realiza OR entre todos los recibidos
	 * y esta negado o no segun el flag de negacion recibido
	 * @throws QueryException si filters esta vacio
	 */
	private static SignatureFilter composeORFilter(Set<SignatureFilter> filters, boolean negated)
			throws QueryException {
		
		if (filters.size() > 1) {
			// Comienzo a anidar varios ORSignatureFilters
			SignatureORFilter or = null;
			Iterator<SignatureFilter> it = filters.iterator();
			while (it.hasNext()) {
				if (or == null) {
					SignatureFilter f1 = it.next();
					SignatureFilter f2 = it.next();
					or = new SignatureORFilter(f1, f2, false);
				} else {
					SignatureFilter f = it.next();
					or = new SignatureORFilter(or, f, false);
				}
			}
			// Niego el OR resultante si corresponde, y lo devuelvo
			if (negated) {
				or.invertNegatedFlag();
			}
			return or;
		} else if (filters.size() == 1) {
			// Devuelvo el unico elemento del conjunto
			// Previamente lo niego si corresponde
			SignatureFilter f = filters.iterator().next();
			if (negated) {
				f.invertNegatedFlag();
			}
			return f;
		} else {
			// Es cero
			throw new QueryException("Error de sintaxis en la consulta");
		}
	}
	
	/**
	 * Crea un SignatureFilter que vincule mediante el operador AND
	 * a todos los que contiene el Set recibido por parametro.
	 * Ademas este filtro estara negado o no segun el flag que tambien
	 * recibe como parametro.
	 * @param filters un conjunto de SignatureFilters
	 * @param negated flag de negacion para el filtro a generar
	 * @return SignatureFilter que realiza AND entre todos los recibidos
	 * y esta negado o no segun el flag de negacion recibido
	 * @throws QueryException si filters esta vacio
	 */
	private static SignatureFilter composeANDFilter(Set<SignatureFilter> filters, boolean negated)
		throws QueryException {
			
			if (filters.size() > 1) {
				// Comienzo a anidar varios ANDSignatureFilters
				SignatureANDFilter and = null;
				Iterator<SignatureFilter> it = filters.iterator();
				while (it.hasNext()) {
					if (and == null) {
						SignatureFilter f1 = it.next();
						SignatureFilter f2 = it.next();
						and = new SignatureANDFilter(f1, f2, false);
					} else {
						SignatureFilter f = it.next();
						and = new SignatureANDFilter(and, f, false);
					}
				}
				// Niego el AND resultante si corresponde, y lo devuelvo
				if (negated) {
					and.invertNegatedFlag();
				}
				return and;
			} else if (filters.size() == 1) {
				// Devuelvo el unico elemento del conjunto
				// Previamente lo niego si corresponde
				SignatureFilter f = filters.iterator().next();
				if (negated) {
					f.invertNegatedFlag();
				}
				return f;
			} else {
				// Es cero
				throw new QueryException("Error de sintaxis en la consulta");
			}
	}
	
	/**
	 * Busca el final de una subquery delimitada por parentesis
	 * @param palabras array de palabras separadas de la consulta
	 * @param from posicion desde donde buscar, la siguiente al parentesis de apertura
	 * @return posicion posterior al parentesis de cierre correspondiente al ubicado
	 * en from-1
	 * @throws QueryException si no existe el parentesis de cierre
	 */
	private static int findSubqueryEnd(String[] palabras, int from) throws QueryException {
		int open = 1;
		int closed = 0;
		
		int i = from;
		while (i < palabras.length && open != closed) {
			if (palabras[i].equals("(")) {
				open++;
			} else if (palabras[i].equals(")")) {
				closed++;
			}
			i++;
		}
		
		if (open == closed) {
			return i-1;
		} else {
			throw new QueryException("Error de sintaxis en la consulta - "
					+ "Falta un parentesis derecho");
		}
	}

}
